From 0b745a6dfa4daebef1d5e1b79a9452a7962375cf Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 29 Mar 2022 08:41:27 +1100 Subject: [PATCH] ssm-browse: new utility to browse SSM parameters This is more of an exercise to work out how best to use controllers --- cmd/ssm-browse/main.go | 44 ++++++++++++ go.mod | 9 +-- go.sum | 10 +++ internal/common/ui/events/commands.go | 6 +- internal/common/ui/logging/debug.go | 18 +++++ internal/ssm-browse/controllers/events.go | 7 ++ .../ssm-browse/controllers/ssmcontroller.go | 31 +++++++++ internal/ssm-browse/models/models.go | 10 +++ .../ssm-browse/providers/awsssm/provider.go | 42 +++++++++++ .../services/ssmparameters/iface.go | 10 +++ .../services/ssmparameters/service.go | 20 ++++++ internal/ssm-browse/ui/model.go | 55 +++++++++++++++ internal/ssm-browse/ui/ssmlist/ssmlist.go | 69 +++++++++++++++++++ internal/ssm-browse/ui/ssmlist/tblmodel.go | 22 ++++++ 14 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 cmd/ssm-browse/main.go create mode 100644 internal/common/ui/logging/debug.go create mode 100644 internal/ssm-browse/controllers/events.go create mode 100644 internal/ssm-browse/controllers/ssmcontroller.go create mode 100644 internal/ssm-browse/models/models.go create mode 100644 internal/ssm-browse/providers/awsssm/provider.go create mode 100644 internal/ssm-browse/services/ssmparameters/iface.go create mode 100644 internal/ssm-browse/services/ssmparameters/service.go create mode 100644 internal/ssm-browse/ui/model.go create mode 100644 internal/ssm-browse/ui/ssmlist/ssmlist.go create mode 100644 internal/ssm-browse/ui/ssmlist/tblmodel.go diff --git a/cmd/ssm-browse/main.go b/cmd/ssm-browse/main.go new file mode 100644 index 0000000..f08af42 --- /dev/null +++ b/cmd/ssm-browse/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/lmika/awstools/internal/common/ui/logging" + "github.com/lmika/awstools/internal/ssm-browse/controllers" + "github.com/lmika/awstools/internal/ssm-browse/providers/awsssm" + "github.com/lmika/awstools/internal/ssm-browse/services/ssmparameters" + "github.com/lmika/awstools/internal/ssm-browse/ui" + "github.com/lmika/gopkgs/cli" + "os" +) + +func main() { + // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. + lipgloss.HasDarkBackground() + + closeFn := logging.EnableLogging() + defer closeFn() + + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + cli.Fatalf("cannot load AWS config: %v", err) + } + ssmClient := ssm.NewFromConfig(cfg) + + provider := awsssm.NewProvider(ssmClient) + service := ssmparameters.NewService(provider) + + ctrl := controllers.New(service) + model := ui.NewModel(ctrl) + + p := tea.NewProgram(model, tea.WithAltScreen()) + + if err := p.Start(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index e259caa..da7b341 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/alecthomas/participle/v2 v2.0.0-alpha7 github.com/asdine/storm v2.1.2+incompatible - github.com/aws/aws-sdk-go-v2 v1.15.0 + github.com/aws/aws-sdk-go-v2 v1.16.1 github.com/aws/aws-sdk-go-v2/config v1.13.1 github.com/aws/aws-sdk-go-v2/credentials v1.8.0 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0 @@ -27,16 +27,17 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect - github.com/aws/smithy-go v1.11.1 // indirect + github.com/aws/smithy-go v1.11.2 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index 23fc8c7..527818a 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/aws/aws-sdk-go-v2 v1.13.0 h1:1XIXAfxsEmbhbj5ry3D3vX+6ZcUYvIqSm4CWWEuG github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w= github.com/aws/aws-sdk-go-v2 v1.15.0 h1:f9kWLNfyCzCB43eupDAk3/XgJ2EpgktiySD6leqs0js= github.com/aws/aws-sdk-go-v2 v1.15.0/go.mod h1:lJYcuZZEHWNIb6ugJjbQY1fykdoobWbOS7kJYb4APoI= +github.com/aws/aws-sdk-go-v2 v1.16.1 h1:udzee98w8H6ikRgtFdVN9JzzYEbi/quFfSvduZETJIU= +github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo= github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs= github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o= @@ -22,10 +24,14 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 h1:CRiQJ4E2RhfDdqbie1 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 h1:xiGjGVQsem2cxoIX61uRGy+Jux2s9C/kKbTrWLdrU54= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6/go.mod h1:SSPEdf9spsFgJyhjrXvawfpyzrXHBCUe+2eQ1CjC1Ak= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8 h1:CDaO90VZVBAL1sK87S5oSPIrp7yZqORv1hPIi2UsTMk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 h1:3ADoioDMOtF4uiK59vCpplpCwugEU+v4ZFD29jDL3RQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 h1:bt3zw79tm209glISdMRCIVRCwvSDXxgAxh5KWe2qHkY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0/go.mod h1:viTrxhAuejD+LszDahzAE2x40YjYWhMqzHxv2ZiWaME= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2 h1:XXR3cdOcKRCTZf6ctcqpMf+go1BdzTm6+T9Ul5zxcMI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 h1:qnx+WyIH9/AD+wAxi05WCMNanO236ceqHg6hChCWs3M= @@ -40,6 +46,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU= github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk= github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0/go.mod h1:IBTQMG8mtyj37OWg7vIXcg714Ntcb/LlYou/rZpvV1k= +github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 h1:p22U2yL/AeRToERGcZv1R26Yci5VQnWIrpzcZdG54cg= +github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0/go.mod h1:chcyLYBEVRac/7rWJsD6cUHUR2osROwavvNqCplfwog= github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA= github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU= github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E= @@ -48,6 +56,8 @@ github.com/aws/smithy-go v1.10.0 h1:gsoZQMNHnX+PaghNw4ynPsyGP7aUCqx5sY2dlPQsZ0w= github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.11.1 h1:IQ+lPZVkSM3FRtyaDox41R8YS6iwPMYIreejOgPW49g= github.com/aws/smithy-go v1.11.1/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= +github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE= +github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/brianvoe/gofakeit/v6 v6.15.0 h1:lJPGJZ2/07TRGDazyTzD5b18N3y4tmmJpdhCUw18FlI= github.com/brianvoe/gofakeit/v6 v6.15.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/calyptia/go-bubble-table v0.1.0 h1:mXpaaBlrHGH4K8v5PvM8YqBFT9jlysS1YOycU2u3gEQ= diff --git a/internal/common/ui/events/commands.go b/internal/common/ui/events/commands.go index b7e6344..6a679de 100644 --- a/internal/common/ui/events/commands.go +++ b/internal/common/ui/events/commands.go @@ -1,8 +1,12 @@ package events -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "log" +) func Error(err error) tea.Msg { + log.Println(err) return ErrorMsg(err) } diff --git a/internal/common/ui/logging/debug.go b/internal/common/ui/logging/debug.go new file mode 100644 index 0000000..37decab --- /dev/null +++ b/internal/common/ui/logging/debug.go @@ -0,0 +1,18 @@ +package logging + +import ( + "fmt" + tea "github.com/charmbracelet/bubbletea" + "os" +) + +func EnableLogging() (closeFn func()) { + f, err := tea.LogToFile("debug.log", "debug") + if err != nil { + fmt.Println("fatal:", err) + os.Exit(1) + } + return func() { + f.Close() + } +} diff --git a/internal/ssm-browse/controllers/events.go b/internal/ssm-browse/controllers/events.go new file mode 100644 index 0000000..d629930 --- /dev/null +++ b/internal/ssm-browse/controllers/events.go @@ -0,0 +1,7 @@ +package controllers + +import "github.com/lmika/awstools/internal/ssm-browse/models" + +type NewParameterListMsg struct { + Parameters *models.SSMParameters +} diff --git a/internal/ssm-browse/controllers/ssmcontroller.go b/internal/ssm-browse/controllers/ssmcontroller.go new file mode 100644 index 0000000..bfe5f5d --- /dev/null +++ b/internal/ssm-browse/controllers/ssmcontroller.go @@ -0,0 +1,31 @@ +package controllers + +import ( + "context" + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/awstools/internal/common/ui/events" + "github.com/lmika/awstools/internal/ssm-browse/services/ssmparameters" +) + +type SSMController struct { + service *ssmparameters.Service +} + +func New(service *ssmparameters.Service) *SSMController { + return &SSMController{ + service: service, + } +} + +func (c *SSMController) Fetch() tea.Cmd { + return func() tea.Msg { + res, err := c.service.List(context.Background()) + if err != nil { + return events.Error(err) + } + + return NewParameterListMsg{ + Parameters: res, + } + } +} diff --git a/internal/ssm-browse/models/models.go b/internal/ssm-browse/models/models.go new file mode 100644 index 0000000..777e6c8 --- /dev/null +++ b/internal/ssm-browse/models/models.go @@ -0,0 +1,10 @@ +package models + +type SSMParameters struct { + Items []SSMParameter +} + +type SSMParameter struct { + Name string + Value string +} diff --git a/internal/ssm-browse/providers/awsssm/provider.go b/internal/ssm-browse/providers/awsssm/provider.go new file mode 100644 index 0000000..15f3fec --- /dev/null +++ b/internal/ssm-browse/providers/awsssm/provider.go @@ -0,0 +1,42 @@ +package awsssm + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/lmika/awstools/internal/ssm-browse/models" + "github.com/pkg/errors" +) + +type Provider struct { + client *ssm.Client +} + +func NewProvider(client *ssm.Client) *Provider { + return &Provider{ + client: client, + } +} + +func (p *Provider) List(ctx context.Context) (*models.SSMParameters, error) { + pars, err := p.client.GetParametersByPath(ctx, &ssm.GetParametersByPathInput{ + Path: aws.String("/"), + MaxResults: 10, + Recursive: true, + }) + if err != nil { + return nil, errors.Wrap(err, "cannot get parameters from path") + } + + res := &models.SSMParameters{ + Items: make([]models.SSMParameter, len(pars.Parameters)), + } + for i, p := range pars.Parameters { + res.Items[i] = models.SSMParameter{ + Name: aws.ToString(p.Name), + Value: aws.ToString(p.Value), + } + } + + return res, nil +} diff --git a/internal/ssm-browse/services/ssmparameters/iface.go b/internal/ssm-browse/services/ssmparameters/iface.go new file mode 100644 index 0000000..ed3eb47 --- /dev/null +++ b/internal/ssm-browse/services/ssmparameters/iface.go @@ -0,0 +1,10 @@ +package ssmparameters + +import ( + "context" + "github.com/lmika/awstools/internal/ssm-browse/models" +) + +type SSMProvider interface { + List(ctx context.Context) (*models.SSMParameters, error) +} diff --git a/internal/ssm-browse/services/ssmparameters/service.go b/internal/ssm-browse/services/ssmparameters/service.go new file mode 100644 index 0000000..16b9c7c --- /dev/null +++ b/internal/ssm-browse/services/ssmparameters/service.go @@ -0,0 +1,20 @@ +package ssmparameters + +import ( + "context" + "github.com/lmika/awstools/internal/ssm-browse/models" +) + +type Service struct { + provider SSMProvider +} + +func NewService(provider SSMProvider) *Service { + return &Service{ + provider: provider, + } +} + +func (s *Service) List(ctx context.Context) (*models.SSMParameters, error) { + return s.provider.List(ctx) +} \ No newline at end of file diff --git a/internal/ssm-browse/ui/model.go b/internal/ssm-browse/ui/model.go new file mode 100644 index 0000000..d11585a --- /dev/null +++ b/internal/ssm-browse/ui/model.go @@ -0,0 +1,55 @@ +package ui + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt" + "github.com/lmika/awstools/internal/ssm-browse/controllers" + "github.com/lmika/awstools/internal/ssm-browse/ui/ssmlist" +) + +type Model struct { + controller *controllers.SSMController + + root tea.Model + ssmList *ssmlist.Model +} + +func NewModel(controller *controllers.SSMController) Model { + ssmList := ssmlist.New() + root := layout.FullScreen( + statusandprompt.New(ssmList, "Hello SSM"), + ) + + return Model{ + controller: controller, + root: root, + ssmList: ssmList, + } +} + + +func (m Model) Init() tea.Cmd { + return m.controller.Fetch() +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case controllers.NewParameterListMsg: + m.ssmList.SetParameters(msg.Parameters) + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + } + } + + newRoot, cmd := m.root.Update(msg) + m.root = newRoot + return m, cmd +} + +func (m Model) View() string { + return m.root.View() +} + diff --git a/internal/ssm-browse/ui/ssmlist/ssmlist.go b/internal/ssm-browse/ui/ssmlist/ssmlist.go new file mode 100644 index 0000000..ae2d0c6 --- /dev/null +++ b/internal/ssm-browse/ui/ssmlist/ssmlist.go @@ -0,0 +1,69 @@ +package ssmlist + +import ( + table "github.com/calyptia/go-bubble-table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/awstools/internal/ssm-browse/models" +) + +type Model struct { + frameTitle frame.FrameTitle + table table.Model + + parameters *models.SSMParameters + + w, h int +} + +func New() *Model { + frameTitle := frame.NewFrameTitle("SSM", true) + table := table.New([]string{"name", "type", "value"}, 0, 0) + + return &Model{ + frameTitle: frameTitle, + table: table, + } +} + +func (m *Model) SetParameters(parameters *models.SSMParameters) { + m.parameters = parameters + cols := []string{"name", "type", "value"} + + newTbl := table.New(cols, m.w, m.h-m.frameTitle.HeaderHeight()) + newRows := make([]table.Row, len(parameters.Items)) + for i, r := range parameters.Items { + newRows[i] = itemTableRow{r} + } + newTbl.SetRows(newRows) + + m.table = newTbl +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + m.table, cmd = m.table.Update(msg) + return m, cmd + } + return m, nil +} + +func (m *Model) View() string { + return lipgloss.JoinVertical(lipgloss.Top, m.frameTitle.View(), m.table.View()) +} + +func (m *Model) Resize(w, h int) layout.ResizingModel { + m.w, m.h = w, h + m.frameTitle.Resize(w, h) + m.table.SetSize(w, h - m.frameTitle.HeaderHeight()) + return m +} + diff --git a/internal/ssm-browse/ui/ssmlist/tblmodel.go b/internal/ssm-browse/ui/ssmlist/tblmodel.go new file mode 100644 index 0000000..c9f78aa --- /dev/null +++ b/internal/ssm-browse/ui/ssmlist/tblmodel.go @@ -0,0 +1,22 @@ +package ssmlist + +import ( + "fmt" + table "github.com/calyptia/go-bubble-table" + "github.com/lmika/awstools/internal/ssm-browse/models" + "io" +) + +type itemTableRow struct { + item models.SSMParameter +} + +func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) { + line := fmt.Sprintf("%s\t%s\t%s", mtr.item.Name, "String", mtr.item.Value) + + if index == model.Cursor() { + fmt.Fprintln(w, model.Styles.SelectedRow.Render(line)) + } else { + fmt.Fprintln(w, line) + } +}