diff --git a/app/application.go b/app/application.go index baca745..2186f53 100644 --- a/app/application.go +++ b/app/application.go @@ -2,7 +2,7 @@ package app import ( "github.com/starkandwayne/carousel/credhub" - "github.com/starkandwayne/carousel/store" + "github.com/starkandwayne/carousel/state" "github.com/gdamore/tcell/v2" @@ -11,11 +11,12 @@ import ( type Application struct { *tview.Application - store *store.Store + state state.State credhub credhub.CredHub layout *Layout keyBindings map[tcell.Key]func() selectedID string + refresh func() } type Layout struct { @@ -24,12 +25,13 @@ type Layout struct { details *tview.Flex } -func NewApplication(store *store.Store, ch credhub.CredHub) *Application { +func NewApplication(state state.State, ch credhub.CredHub, refresh func()) *Application { return &Application{ Application: tview.NewApplication(), - store: store, + state: state, keyBindings: make(map[tcell.Key]func(), 0), credhub: ch, + refresh: refresh, } } diff --git a/app/details.go b/app/details.go index d88c43c..9e6a6c8 100644 --- a/app/details.go +++ b/app/details.go @@ -8,7 +8,8 @@ import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/starkandwayne/carousel/store" + "github.com/starkandwayne/carousel/credhub" + "github.com/starkandwayne/carousel/state" "gopkg.in/yaml.v2" "github.com/grantae/certinfo" @@ -24,7 +25,7 @@ func (a *Application) actionShowDetails(ref interface{}) { a.layout.details.Clear().AddItem(a.renderDetailsFor(ref), 0, 1, false) } -func (a *Application) actionToggleTransitional(cred *store.Credential) { +func (a *Application) actionToggleTransitional(cred *state.Credential) { modal := tview.NewModal(). SetText(fmt.Sprintf("Set transitional=%s for %s@%s", strconv.FormatBool(!cred.Transitional), @@ -37,7 +38,7 @@ func (a *Application) actionToggleTransitional(cred *store.Credential) { panic(err) } a.statusModal("Refreshing State...") - err = a.store.Refresh() + a.refresh() if err != nil { panic(err) } @@ -53,16 +54,16 @@ func (a *Application) actionToggleTransitional(cred *store.Credential) { func (a *Application) renderDetailsFor(ref interface{}) tview.Primitive { switch v := ref.(type) { - case *store.Path: + case *state.Path: return a.renderPathDetail(v) - case *store.Credential: + case *state.Credential: return a.renderCredentialDetail(v) default: return a.renderWelcome() } } -func (a *Application) renderPathDetail(p *store.Path) tview.Primitive { +func (a *Application) renderPathDetail(p *state.Path) tview.Primitive { t := tview.NewTable() t.SetBorder(true) t.SetTitle("Credhub & BOSH") @@ -90,31 +91,47 @@ func (a *Application) renderPathDetail(p *store.Path) tview.Primitive { AddItem(info, 0, 1, true) } -func (a *Application) renderCredentialDetail(cred *store.Credential) tview.Primitive { +func (a *Application) renderCredentialDetail(cred *state.Credential) tview.Primitive { t := tview.NewTable() t.SetBorder(true) t.SetTitle("Credhub & BOSH") addSimpleRow(t, "ID", cred.ID) - addSimpleRow(t, "Expiry", fmt.Sprintf("%s (%s)", - cred.ExpiryDate.Format(time.RFC3339), - humanize.RelTime(*cred.ExpiryDate, time.Now(), "ago", "from now"))) - addSimpleRow(t, "Transitional", strconv.FormatBool(cred.Transitional)) - addSimpleRow(t, "Certificate Authority", strconv.FormatBool(cred.CertificateAuthority)) - addSimpleRow(t, "Self Signed", strconv.FormatBool(cred.SelfSigned)) - + addSimpleRow(t, "Created At", fmt.Sprintf("%s (%s)", + cred.VersionCreatedAt.Format(time.RFC3339), + humanize.RelTime(*cred.VersionCreatedAt, time.Now(), "ago", "from now"))) addSimpleRow(t, "Deployments", renderDeployments(cred.Deployments)) + addSimpleRow(t, "Latest", strconv.FormatBool(cred.Latest)) - i, err := certinfo.CertificateText(cred.Certificate) - if err != nil { - panic(err) - } + var info *tview.TextView + detailRows := 4 + 2 // 2 for top and bottom border - info := tview.NewTextView().SetText(i). - SetTextColor(tcell.Color102) + switch cred.Type { + case credhub.Certificate: + addSimpleRow(t, "Expiry", fmt.Sprintf("%s (%s)", + cred.ExpiryDate.Format(time.RFC3339), + humanize.RelTime(*cred.ExpiryDate, time.Now(), "ago", "from now"))) + addSimpleRow(t, "Transitional", strconv.FormatBool(cred.Transitional)) + addSimpleRow(t, "Certificate Authority", strconv.FormatBool(cred.CertificateAuthority)) + addSimpleRow(t, "Self Signed", strconv.FormatBool(cred.SelfSigned)) - info.SetBorder(true) - info.SetTitle("Raw Certificate") + detailRows = detailRows + 4 + + i, err := certinfo.CertificateText(cred.Certificate) + if err != nil { + panic(err) + } + + info = tview.NewTextView().SetText(i). + SetTextColor(tcell.Color102) + info.SetBorder(true) + info.SetTitle("Raw Certificate") + default: + info = tview.NewTextView().SetText("TODO"). + SetTextColor(tcell.Color102) + info.SetBorder(true) + info.SetTitle("Info") + } a.layout.tree.SetInputCapture(a.nextFocusInputCaptureHandler(t)) t.SetInputCapture(a.nextFocusInputCaptureHandler(info)) @@ -122,12 +139,12 @@ func (a *Application) renderCredentialDetail(cred *store.Credential) tview.Primi return tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(t, 8, 1, false). + AddItem(t, detailRows, 1, false). AddItem(a.renderCredentialActions(cred), 1, 1, false). AddItem(info, 0, 1, true) } -func (a *Application) renderCredentialActions(cred *store.Credential) tview.Primitive { +func (a *Application) renderCredentialActions(cred *state.Credential) tview.Primitive { actions := []string{ "Toggle Transitional", "Delete", @@ -148,7 +165,7 @@ func (a *Application) renderCredentialActions(cred *store.Credential) tview.Prim SetText(" " + strings.Join(out, " ")) } -func (a *Application) renderPathActions(p *store.Path) tview.Primitive { +func (a *Application) renderPathActions(p *state.Path) tview.Primitive { actions := []string{ "Regenerate", "Delete", @@ -186,7 +203,7 @@ func addSimpleRow(t *tview.Table, lbl, val string) { t.SetCellSimple(row, 1, val) } -func renderDeployments(deployments []*store.Deployment) string { +func renderDeployments(deployments []*state.Deployment) string { tmp := make([]string, 0) for _, d := range deployments { tmp = append(tmp, d.Name) diff --git a/app/helpers.go b/app/helpers.go new file mode 100644 index 0000000..5c1c985 --- /dev/null +++ b/app/helpers.go @@ -0,0 +1,21 @@ +package app + +import ( + "time" + + "github.com/starkandwayne/carousel/state" +) + +func toStatus(c *state.Credential) string { + status := "active" + if c.ExpiryDate != nil && c.ExpiryDate.Sub(time.Now()) < time.Hour*24*30 { + status = "notice" + } + if c.VersionCreatedAt.Sub(time.Now()) > time.Hour*24*365 { + status = "notice" + } + if len(c.Deployments) == 0 { + status = "unused" + } + return status +} diff --git a/app/tree.go b/app/tree.go index e80c6ff..b418bcb 100644 --- a/app/tree.go +++ b/app/tree.go @@ -6,7 +6,8 @@ import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "github.com/starkandwayne/carousel/store" + "github.com/starkandwayne/carousel/credhub" + "github.com/starkandwayne/carousel/state" ) const treePanel = "TreePanel" @@ -17,13 +18,24 @@ func (a *Application) viewTree() *tview.TreeView { func (a *Application) renderTree() { root := tview.NewTreeNode("∎") - a.store.EachPath(func(path *store.Path) { - // only interested in top level certVersions - if len(path.Versions) != 0 && path.Versions[0].SignedBy != nil { - return + + for _, credType := range credhub.CredentialTypes { + credentials := a.state.Credentials( + state.TypeFilter(credType), + state.SelfSignedFilter(), + state.LatestFilter()) + + if len(credentials) == 0 { + continue } - root.SetChildren(append(root.GetChildren(), addToTree(path.Versions)...)) - }) + + typeNode := tview.NewTreeNode(string(credType)).Collapse() + root.AddChild(typeNode) + + for _, credential := range credentials { + typeNode.SetChildren(append(typeNode.GetChildren(), addToTree(credential.Path.Versions)...)) + } + } var currentNode *tview.TreeNode @@ -37,6 +49,7 @@ func (a *Application) renderTree() { } if refToID(node.GetReference()) == a.selectedID { currentNode = node + root.ExpandAll() a.actionShowDetails(currentNode.GetReference()) return false } @@ -57,16 +70,16 @@ func (a *Application) renderTree() { func refToID(ref interface{}) string { switch v := ref.(type) { - case *store.Credential: + case *state.Credential: return v.ID - case *store.Path: + case *state.Path: return v.Name default: return "" } } -func addToTree(creds []*store.Credential) []*tview.TreeNode { +func addToTree(creds []*state.Credential) []*tview.TreeNode { out := make([]*tview.TreeNode, 0) for _, cred := range creds { pathNode := tview.NewTreeNode(cred.Path.Name). @@ -80,12 +93,12 @@ func addToTree(creds []*store.Credential) []*tview.TreeNode { } } - lbl := fmt.Sprintf("%s (%s)", cred.ID, cred.Status()) + lbl := fmt.Sprintf("%s (%s)", cred.ID, toStatus(cred)) if cred.Transitional { lbl = lbl + " (transitional)" } credNode := tview.NewTreeNode(lbl).SetReference(cred) - switch cred.Status() { + switch toStatus(cred) { case "unused": credNode.SetColor(tcell.Color102) case "notice": diff --git a/store/store_suite_test.go b/cmd/cmd_suite_test.go similarity index 58% rename from store/store_suite_test.go rename to cmd/cmd_suite_test.go index aa685e1..22d1c57 100644 --- a/store/store_suite_test.go +++ b/cmd/cmd_suite_test.go @@ -1,4 +1,4 @@ -package store_test +package cmd_test import ( "testing" @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestStore(t *testing.T) { +func TestCmd(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Store Suite") + RunSpecs(t, "Cmd Suite") } diff --git a/cmd/ui.go b/cmd/ui.go index 77854dc..c39322e 100644 --- a/cmd/ui.go +++ b/cmd/ui.go @@ -33,13 +33,9 @@ This application is a tool to generate the needed files to quickly create a Cobra application.`, Run: func(cmd *cobra.Command, args []string) { initialize() + refresh() - err := store.Refresh() - if err != nil { - logger.Fatalf("failed to load data: %s", err) - } - - app := app.NewApplication(store, credhub).Init() + app := app.NewApplication(state, credhub, refresh).Init() if err := app.Run(); err != nil { logger.Fatalf("the ui encountered an error: %s", err) diff --git a/credhub/types.go b/credhub/types.go index 6c95ecf..5bb9a3b 100644 --- a/credhub/types.go +++ b/credhub/types.go @@ -14,6 +14,10 @@ const ( "ssh", "rsa", "password", "user", "value", "json" ) +var CredentialTypes = []CredentialType{ + Certificate, SSH, RSA, Password, User, Value, JSON, +} + type Credential struct { ID string `json:"id"` Metadata Metadata `json:"metadata,omitempty"` diff --git a/state/credentials.go b/state/credentials.go index 75353d3..4b79d1a 100644 --- a/state/credentials.go +++ b/state/credentials.go @@ -1,20 +1,6 @@ package state -import "github.com/starkandwayne/carousel/credhub" - -func TypeFilter(types ...credhub.CredentialType) Filter { - return func(c *Credential) bool { - match := false - for _, t := range types { - if c.Type == t { - match = true - } - } - return match - } -} - -func (s *state) Credentials(filters ...Filter) []*Credential { +func (s *state) Credentials(filters ...filter) []*Credential { certs := s.credentials.Select(func(_, v interface{}) bool { for _, fn := range filters { if !fn(v.(*Credential)) { diff --git a/state/filters.go b/state/filters.go new file mode 100644 index 0000000..37df6b5 --- /dev/null +++ b/state/filters.go @@ -0,0 +1,31 @@ +package state + +import ( + "github.com/starkandwayne/carousel/credhub" +) + +type filter func(*Credential) bool + +func SelfSignedFilter() filter { + return func(c *Credential) bool { + return c.SignedBy == nil + } +} + +func LatestFilter() filter { + return func(c *Credential) bool { + return c.Latest + } +} + +func TypeFilter(types ...credhub.CredentialType) filter { + return func(c *Credential) bool { + match := false + for _, t := range types { + if c.Type == t { + match = true + } + } + return match + } +} diff --git a/state/state.go b/state/state.go index 0e62a44..c6c480a 100644 --- a/state/state.go +++ b/state/state.go @@ -7,11 +7,9 @@ import ( "github.com/starkandwayne/carousel/credhub" ) -type Filter func(*Credential) bool - type State interface { Update([]*credhub.Credential, []*bosh.Variable) error - Credentials(...Filter) []*Credential + Credentials(...filter) []*Credential } func NewState() State { diff --git a/store/load.go b/store/load.go deleted file mode 100644 index ae2e2ed..0000000 --- a/store/load.go +++ /dev/null @@ -1,198 +0,0 @@ -package store - -import ( - "bytes" - "fmt" - "path" - "strconv" - - "github.com/emirpasic/gods/maps/treebidimap" - "github.com/emirpasic/gods/utils" - "github.com/starkandwayne/carousel/credhub" - "gopkg.in/yaml.v2" - - boshdir "github.com/cloudfoundry/bosh-cli/director" -) - -func NewStore(ch credhub.CredHub, directorClient boshdir.Director) *Store { - return &Store{ - paths: treebidimap.NewWith(utils.StringComparator, pathByName), - credentials: treebidimap.NewWith(utils.StringComparator, credentialById), - deployments: treebidimap.NewWith(utils.StringComparator, deploymentByName), - variableDefinitions: treebidimap.NewWith(utils.StringComparator, veriableDefinitionByName), - credhub: ch, - directorClient: directorClient, - } -} - -func (s *Store) Refresh() error { - s.paths.Clear() - s.credentials.Clear() - s.deployments.Clear() - s.variableDefinitions.Clear() - - creds, err := s.credhub.FindAll() - if err != nil { - return err - } - - for _, cred := range creds { - var path *Path - p, found := s.paths.Get(cred.Name) - if found { - path = p.(*Path) - } else { - path = &Path{Name: cred.Name} - s.paths.Put(cred.Name, path) - } - - c := Credential{ - Credential: cred, - Deployments: make([]*Deployment, 0), - Path: path, - } - - path.AppendVersion(&c) - s.credentials.Put(cred.ID, &c) - } - - // Lookup Ca for each cert - for _, cert := range s.Certificates() { - authorityKeyID := cert.Certificate.AuthorityKeyId - if cert.SelfSigned { - continue - } - ca, found := s.getCertVersionBySubjectKeyId(authorityKeyID) - if found { - ca.Signs = append(ca.Signs, cert) - cert.SignedBy = ca - } else { - return fmt.Errorf("failed to lookup ca Credential with id: %s", cert.ID) - } - } - - // Mark last Credential per Path as Latest - s.EachPath(func(p *Path) { - latest := p.Versions[0] // There can never be a path without at least one version - for _, c := range p.Versions { - if latest.VersionCreatedAt.Before(*c.VersionCreatedAt) { - latest = c - } - } - latest.Latest = true - }) - - directorInfo, err := s.directorClient.Info() - if err != nil { - return err - } - - deployments, err := s.directorClient.Deployments() - if err != nil { - return err - } - for _, deployment := range deployments { - d := Deployment{ - Name: deployment.Name(), - Versions: make([]*Credential, 0), - } - s.deployments.Put(d.Name, &d) - variables, err := deployment.Variables() - if err != nil { - return err - } - for _, variable := range variables { - credential, found := s.GetCredential(variable.ID) - if !found { - return fmt.Errorf("failed to lookup credential for bosh variable with id: %s", - variable.ID) - } - credential.Deployments = append(credential.Deployments, &d) - d.Versions = append(d.Versions, credential) - } - - rawDeploymentManifest, err := deployment.Manifest() - if err != nil { - return err - } - - varDefs, err := rawManifestToVariableDefinitions(rawDeploymentManifest) - if err != nil { - return err - } - - for _, varDef := range varDefs { - name := path.Join("/", directorInfo.Name, deployment.Name(), varDef.Name) - s.variableDefinitions.Put(name, varDef) - - p, found := s.GetPath(name) - if !found { - return fmt.Errorf("failed to lookup path for variable definiton with name: %s", - name) - } - p.VariableDefinition = varDef - } - - configs, err := s.directorClient.ListDeploymentConfigs(d.Name) - if err != nil { - return err - } - - for _, conf := range configs.GetConfigs() { - if conf.Type == "runtime" { - c, err := s.directorClient.LatestConfigByID(strconv.Itoa(conf.Id)) - if err != nil { - return err - } - - varDefs, err := rawManifestToVariableDefinitions(c.Content) - if err != nil { - return err - } - - for _, varDef := range varDefs { - s.variableDefinitions.Put(varDef.Name, varDef) - - p, found := s.GetPath(varDef.Name) - if !found { - return fmt.Errorf("failed to lookup path for variable definiton with name: %s", - varDef.Name) - - } - p.VariableDefinition = varDef - } - - } - } - } - - return nil -} - -func rawManifestToVariableDefinitions(raw string) ([]*VariableDefinition, error) { - tmpl := manifest{} - - err := yaml.Unmarshal([]byte(raw), &tmpl) - if err != nil { - return nil, err - } - - return tmpl.Variables, nil -} - -type manifest struct { - Variables []*VariableDefinition `yaml:"variables"` -} - -func (s *Store) getCertVersionBySubjectKeyId(keyId []byte) (*Credential, bool) { - _, foundValue := s.credentials.Find(func(index interface{}, value interface{}) bool { - if value.(*Credential).Certificate != nil { - return bytes.Compare(value.(*Credential).Certificate.SubjectKeyId, keyId) == 0 - } - return false - }) - if foundValue != nil { - return foundValue.(*Credential), true - } - return nil, false -} diff --git a/store/store.go b/store/store.go deleted file mode 100644 index 1eae664..0000000 --- a/store/store.go +++ /dev/null @@ -1,116 +0,0 @@ -package store - -import ( - "github.com/emirpasic/gods/maps/treebidimap" - "github.com/starkandwayne/carousel/credhub" - - boshdir "github.com/cloudfoundry/bosh-cli/director" -) - -type Store struct { - deployments *treebidimap.Map - paths *treebidimap.Map - credentials *treebidimap.Map - variableDefinitions *treebidimap.Map - credhub credhub.CredHub - directorClient boshdir.Director -} - -func (s *Store) GetPath(path string) (*Path, bool) { - p, found := s.paths.Get(path) - if !found { - return nil, found - } - return p.(*Path), true -} - -func (s *Store) EachPath(fn func(*Path)) { - s.paths.Each(func(_, v interface{}) { - fn(v.(*Path)) - }) -} - -func (s *Store) GetCredential(id string) (*Credential, bool) { - c, found := s.credentials.Get(id) - if !found { - return nil, found - } - return c.(*Credential), true -} - -func (s *Store) EachCredential(fn func(v *Credential)) { - s.credentials.Each(func(_, v interface{}) { - fn(v.(*Credential)) - }) -} - -func (s *Store) Certificates() []*Credential { - certs := s.credentials.Select(func(_, v interface{}) bool { - return v.(*Credential).Type == credhub.Certificate - }) - out := make([]*Credential, 0) - for _, cert := range certs.Values() { - out = append(out, cert.(*Credential)) - } - return out -} - -func pathByName(a, b interface{}) int { - // Type assertion, program will panic if this is not respected - c1 := a.(*Path) - c2 := b.(*Path) - - switch { - case c1.Name > c2.Name: - return 1 - case c1.Name < c2.Name: - return -1 - default: - return 0 - } -} - -func credentialById(a, b interface{}) int { - // Type assertion, program will panic if this is not respected - c1 := a.(*Credential) - c2 := b.(*Credential) - - switch { - case c1.ID > c2.ID: - return 1 - case c1.ID < c2.ID: - return -1 - default: - return 0 - } -} - -func deploymentByName(a, b interface{}) int { - // Type assertion, program will panic if this is not respected - c1 := a.(*Deployment) - c2 := b.(*Deployment) - - switch { - case c1.Name > c2.Name: - return 1 - case c1.Name < c2.Name: - return -1 - default: - return 0 - } -} - -func veriableDefinitionByName(a, b interface{}) int { - // Type assertion, program will panic if this is not respected - c1 := a.(*VariableDefinition) - c2 := b.(*VariableDefinition) - - switch { - case c1.Name > c2.Name: - return 1 - case c1.Name < c2.Name: - return -1 - default: - return 0 - } -} diff --git a/store/types.go b/store/types.go deleted file mode 100644 index 7b445b0..0000000 --- a/store/types.go +++ /dev/null @@ -1,98 +0,0 @@ -package store - -import ( - "encoding/json" - "time" - - "github.com/starkandwayne/carousel/credhub" -) - -type Path struct { - Name string `json:"name"` - Versions []*Credential `json:"-"` - VariableDefinition *VariableDefinition `json:"variable_definition"` -} - -func (p *Path) AppendVersion(c *Credential) { - p.Versions = append(p.Versions, c) -} - -type Credential struct { - *credhub.Credential - Deployments []*Deployment `json:"-"` - SignedBy *Credential `json:"-"` - Signs []*Credential `json:"-"` - Latest bool `json:"latest"` - Path *Path `json:"-"` -} - -func (c *Credential) MarshalJSON() ([]byte, error) { - deployments := make([]string, 0) - for _, d := range c.Deployments { - deployments = append(deployments, d.Name) - } - - updateMode := NoOverwrite - if c.Path.VariableDefinition != nil { - updateMode = c.Path.VariableDefinition.UpdateMode - } - - type Alias Credential - return json.Marshal(&struct { - *Alias - DeploymentsList []string `json:"deployments"` - UpdateMode UpdateMode `json:"update_mode"` - }{ - Alias: (*Alias)(c), - DeploymentsList: deployments, - UpdateMode: updateMode, - }) -} - -func (c *Credential) Status() string { - status := "active" - if c.ExpiryDate != nil && c.ExpiryDate.Sub(time.Now()) < time.Hour*24*30 { - status = "notice" - } - if c.VersionCreatedAt.Sub(time.Now()) > time.Hour*24*365 { - status = "notice" - } - if len(c.Deployments) == 0 { - status = "unused" - } - return status -} - -type Deployment struct { - Versions []*Credential `json:"-"` - Name string `json:"name"` -} - -type VariableDefinition struct { - Name string `yaml:"name" json:"name"` - Type string `yaml:"type" json:"type"` - UpdateMode UpdateMode `yaml:"update_mode,omitempty" json:"update_mode,omitempty"` - Options map[string]interface{} `yaml:"options,omitempty" json:"options,omitempty"` -} - -type UpdateMode string - -const ( - NoOverwrite, Overwrite, Converge UpdateMode = "no-overwrite", "overwrite", "converge" -) - -func (v *VariableDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error { - // update_mode [String, optional]: Update mode to use when generating credentials. - // Currently supported update modes are no-overwrite, overwrite, and converge. Defaults to no-overwrite - // https://bosh.io/docs/manifest-v2/#variables - - type VariableDefinitionDefaulted VariableDefinition - var defaults = VariableDefinitionDefaulted{ - UpdateMode: NoOverwrite, - } - - out := defaults - err := unmarshal(&out) - *v = VariableDefinition(out) - return err -}