From 83aa7b7b48ab06549fd5d35687a23bc4d681935b Mon Sep 17 00:00:00 2001 From: JonasG Date: Mon, 3 Feb 2025 22:13:49 +0100 Subject: [PATCH] feat: check cluster connectivity when upserting --- config/config.go | 48 ++-- kadmin/dev.go | 2 +- kadmin/kadmin.go | 4 + kadmin/sarama_kadmin.go | 105 +++++++-- ktea.go | 18 +- ui/components/cmdbar/notifier_cmdbar.go | 1 + .../upsert_cluster_page.go | 158 +++++++++---- .../upsert_cluster_page_test.go | 221 +++++------------- ui/tabs/clusters_tab/clusters_tab.go | 31 ++- ui/tabs/clusters_tab/clusters_tab_test.go | 22 +- 10 files changed, 331 insertions(+), 279 deletions(-) diff --git a/config/config.go b/config/config.go index cd184d3..d2d433f 100644 --- a/config/config.go +++ b/config/config.go @@ -89,33 +89,13 @@ type ClusterRegisterer interface { // // It returns a ClusterRegisteredMsg with the registered cluster. func (c *Config) RegisterCluster(details RegistrationDetails) tea.Msg { - cluster := Cluster{ - Name: details.Name, - Color: details.Color, - BootstrapServers: []string{details.Host}, - } + cluster := ToCluster(details) // When no clusters exist yet, the first one created becomes the active one by default. if len(c.Clusters) == 0 { cluster.Active = true } - if details.AuthMethod == SASLAuthMethod { - cluster.SASLConfig = &SASLConfig{ - Username: details.Username, - Password: details.Password, - SecurityProtocol: details.SecurityProtocol, - } - } - - if details.SchemaRegistry != nil { - cluster.SchemaRegistry = &SchemaRegistryConfig{ - Url: details.SchemaRegistry.Url, - Username: details.SchemaRegistry.Username, - Password: details.SchemaRegistry.Password, - } - } - // did the newly registered cluster update an existing one var isUpdated bool @@ -142,6 +122,31 @@ func (c *Config) RegisterCluster(details RegistrationDetails) tea.Msg { return ClusterRegisteredMsg{&cluster} } +func ToCluster(details RegistrationDetails) Cluster { + cluster := Cluster{ + Name: details.Name, + Color: details.Color, + BootstrapServers: []string{details.Host}, + } + + if details.AuthMethod == SASLAuthMethod { + cluster.SASLConfig = &SASLConfig{ + Username: details.Username, + Password: details.Password, + SecurityProtocol: details.SecurityProtocol, + } + } + + if details.SchemaRegistry != nil { + cluster.SchemaRegistry = &SchemaRegistryConfig{ + Url: details.SchemaRegistry.Url, + Username: details.SchemaRegistry.Username, + Password: details.SchemaRegistry.Password, + } + } + return cluster +} + func (c *Config) ActiveCluster() *Cluster { for _, c := range c.Clusters { if c.Active { @@ -235,5 +240,6 @@ func New(configIO IO) *Config { } func ReLoadConfig() tea.Msg { + // TODO return nil } diff --git a/kadmin/dev.go b/kadmin/dev.go index d40b625..eba8ed4 100644 --- a/kadmin/dev.go +++ b/kadmin/dev.go @@ -5,5 +5,5 @@ package kadmin import "time" func maybeIntroduceLatency() { - time.Sleep(1 * time.Second) + time.Sleep(2 * time.Second) } diff --git a/kadmin/kadmin.go b/kadmin/kadmin.go index 1bea423..4c73fb7 100644 --- a/kadmin/kadmin.go +++ b/kadmin/kadmin.go @@ -1,6 +1,8 @@ package kadmin import ( + tea "github.com/charmbracelet/bubbletea" + "ktea/config" "time" ) @@ -53,6 +55,8 @@ type Kadmin interface { type Instantiator func(cd ConnectionDetails) (Kadmin, error) +type ConnChecker func(cluster *config.Cluster) tea.Msg + func SaramaInstantiator() Instantiator { return func(cd ConnectionDetails) (Kadmin, error) { return NewSaramaKadmin(cd) diff --git a/kadmin/sarama_kadmin.go b/kadmin/sarama_kadmin.go index 3054c2f..a72f887 100644 --- a/kadmin/sarama_kadmin.go +++ b/kadmin/sarama_kadmin.go @@ -2,6 +2,9 @@ package kadmin import ( "github.com/IBM/sarama" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/log" + "ktea/config" "ktea/sradmin" ) @@ -14,27 +17,64 @@ type SaramaKafkaAdmin struct { sra sradmin.SrAdmin } +type ConnectivityCheckStartedMsg struct { + Connected chan bool + Err chan error +} + +func (c *ConnectivityCheckStartedMsg) AwaitCompletion() tea.Msg { + select { + case <-c.Connected: + return ConnectionCheckSucceeded{} + case err := <-c.Err: + return ConnectivityCheckErrMsg{Err: err} + } +} + +type ConnectionCheckSucceeded struct{} + +type ConnectivityCheckErrMsg struct { + Err error +} + +func ToConnectionDetails(cluster *config.Cluster) ConnectionDetails { + var saslConfig *SASLConfig + if cluster.SASLConfig != nil { + saslConfig = &SASLConfig{ + Username: cluster.SASLConfig.Username, + Password: cluster.SASLConfig.Password, + Protocol: SSL, + } + } + + connDetails := ConnectionDetails{ + BootstrapServers: cluster.BootstrapServers, + SASLConfig: saslConfig, + } + return connDetails +} + func NewSaramaKadmin(cd ConnectionDetails) (Kadmin, error) { - config := sarama.NewConfig() - config.Producer.Return.Successes = true - config.Producer.RequiredAcks = sarama.WaitForAll - config.Producer.Partitioner = sarama.NewRoundRobinPartitioner - config.Consumer.Offsets.Initial = sarama.OffsetOldest + cfg := sarama.NewConfig() + cfg.Producer.Return.Successes = true + cfg.Producer.RequiredAcks = sarama.WaitForAll + cfg.Producer.Partitioner = sarama.NewRoundRobinPartitioner + cfg.Consumer.Offsets.Initial = sarama.OffsetOldest if cd.SASLConfig != nil { - config.Net.TLS.Enable = true - config.Net.SASL.Enable = true - config.Net.SASL.Mechanism = sarama.SASLTypePlaintext - config.Net.SASL.User = cd.SASLConfig.Username - config.Net.SASL.Password = cd.SASLConfig.Password + cfg.Net.TLS.Enable = true + cfg.Net.SASL.Enable = true + cfg.Net.SASL.Mechanism = sarama.SASLTypePlaintext + cfg.Net.SASL.User = cd.SASLConfig.Username + cfg.Net.SASL.Password = cd.SASLConfig.Password } - client, err := sarama.NewClient(cd.BootstrapServers, config) + client, err := sarama.NewClient(cd.BootstrapServers, cfg) if err != nil { return nil, err } - admin, err := sarama.NewClusterAdmin(cd.BootstrapServers, config) + admin, err := sarama.NewClusterAdmin(cd.BootstrapServers, cfg) if err != nil { return nil, err } @@ -49,6 +89,45 @@ func NewSaramaKadmin(cd ConnectionDetails) (Kadmin, error) { admin: admin, addrs: cd.BootstrapServers, producer: producer, - config: config, + config: cfg, }, nil } + +func SaramaConnectivityChecker(cluster *config.Cluster) tea.Msg { + connectedChan := make(chan bool) + errChan := make(chan error) + + cd := ToConnectionDetails(cluster) + cfg := sarama.NewConfig() + + if cd.SASLConfig != nil { + cfg.Net.TLS.Enable = true + cfg.Net.SASL.Enable = true + cfg.Net.SASL.Mechanism = sarama.SASLTypePlaintext + cfg.Net.SASL.User = cd.SASLConfig.Username + cfg.Net.SASL.Password = cd.SASLConfig.Password + } + + go doCheckConnectivity(cd, cfg, errChan, connectedChan) + + return ConnectivityCheckStartedMsg{ + Connected: connectedChan, + Err: errChan, + } +} + +func doCheckConnectivity(cd ConnectionDetails, config *sarama.Config, errChan chan error, connectedChan chan bool) { + maybeIntroduceLatency() + c, err := sarama.NewClient(cd.BootstrapServers, config) + if err != nil { + errChan <- err + return + } + defer func(c sarama.Client) { + err := c.Close() + if err != nil { + log.Error("Unable to close connectivity check connection", err) + } + }(c) + connectedChan <- true +} diff --git a/ktea.go b/ktea.go index de71e82..6422b9b 100644 --- a/ktea.go +++ b/ktea.go @@ -140,7 +140,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } else { - t, c := clusters_tab.New(m.ktx) + t, c := clusters_tab.New(m.ktx, kadmin.SaramaConnectivityChecker) m.tabCtrl = t m.tabs.GoToTab(tabs.ClustersTab) return m, c @@ -202,7 +202,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case 3: if m.clustersTabCtrl == nil { var cmd tea.Cmd - m.clustersTabCtrl, cmd = clusters_tab.New(m.ktx) + m.clustersTabCtrl, cmd = clusters_tab.New(m.ktx, kadmin.SaramaConnectivityChecker) cmds = append(cmds, cmd) } m.tabCtrl = m.clustersTabCtrl @@ -235,19 +235,7 @@ func (m *Model) createTabs(cluster *config.Cluster) { // activateCluster creates the kadmin.Model and kadmin.SrAdmin // based on the given cluster func (m *Model) activateCluster(cluster *config.Cluster) error { - var saslConfig *kadmin.SASLConfig - if cluster.SASLConfig != nil { - saslConfig = &kadmin.SASLConfig{ - Username: cluster.SASLConfig.Username, - Password: cluster.SASLConfig.Password, - Protocol: kadmin.SSL, - } - } - - connDetails := kadmin.ConnectionDetails{ - BootstrapServers: cluster.BootstrapServers, - SASLConfig: saslConfig, - } + connDetails := kadmin.ToConnectionDetails(cluster) if ka, err := m.kaInstantiator(connDetails); err != nil { return err } else { diff --git a/ui/components/cmdbar/notifier_cmdbar.go b/ui/components/cmdbar/notifier_cmdbar.go index d2469fe..e85fd9f 100644 --- a/ui/components/cmdbar/notifier_cmdbar.go +++ b/ui/components/cmdbar/notifier_cmdbar.go @@ -27,6 +27,7 @@ func (n *NotifierCmdBar) IsFocussed() bool { } func (n *NotifierCmdBar) Shortcuts() []statusbar.Shortcut { + // TODO return nil } diff --git a/ui/pages/create_cluster_page/upsert_cluster_page.go b/ui/pages/create_cluster_page/upsert_cluster_page.go index f9927c2..75c2ff1 100644 --- a/ui/pages/create_cluster_page/upsert_cluster_page.go +++ b/ui/pages/create_cluster_page/upsert_cluster_page.go @@ -6,9 +6,12 @@ import ( "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "ktea/config" + "ktea/kadmin" "ktea/kontext" "ktea/styles" "ktea/ui" + "ktea/ui/components/cmdbar" + "ktea/ui/components/notifier" "ktea/ui/components/statusbar" "strings" ) @@ -37,8 +40,10 @@ const ( type Model struct { form *huh.Form formValues *FormValues + notifierCmdBar *cmdbar.NotifierCmdBar ktx *kontext.ProgramKtx clusterRegisterer config.ClusterRegisterer + connChecker kadmin.ConnChecker authSelectionState authSelection srSelectionState srSelection state formState @@ -83,12 +88,37 @@ func (m *Model) View(ktx *kontext.ProgramKtx, renderer *ui.Renderer) string { builder.WriteString("\n") views = append(views, renderer.Render(builder.String())) } - views = append(views, renderer.RenderWithStyle(m.form.View(), styles.Form)) + + notifierView := m.notifierCmdBar.View(ktx, renderer) + formView := renderer.RenderWithStyle(m.form.View(), styles.Form) + views = append(views, notifierView, formView) + return ui.JoinVertical(lipgloss.Top, views...) } func (m *Model) Update(msg tea.Msg) tea.Cmd { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case kadmin.ConnectivityCheckStartedMsg: + cmds = append(cmds, msg.AwaitCompletion) + case kadmin.ConnectionCheckSucceeded: + cmds = append(cmds, func() tea.Msg { + details := m.getRegistrationDetails() + return m.clusterRegisterer.RegisterCluster(details) + }) + } + + _, msg, cmd := m.notifierCmdBar.Update(msg) + if cmd != nil { + cmds = append(cmds, cmd) + } + if msg == nil { + return tea.Batch(cmds...) + } + form, cmd := m.form.Update(msg) + cmds = append(cmds, cmd) if f, ok := form.(*huh.Form); ok { m.form = f } @@ -127,50 +157,61 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd { } if m.form.State == huh.StateCompleted && m.state != loading { - m.state = loading - return func() tea.Msg { - var name string - var newName *string - if m.preEditName == nil { // When creating a cluster - name = m.formValues.Name - newName = nil - } else { // When updating a cluster. - name = *m.preEditName - if m.formValues.Name != *m.preEditName { - newName = &m.formValues.Name - } - } + return m.processFormSubmission() + } + return tea.Batch(cmds...) +} - var authMethod config.AuthMethod - var securityProtocol config.SecurityProtocol - if m.formValues.HasSASLAuthMethodSelected() { - authMethod = config.SASLAuthMethod - securityProtocol = m.formValues.SecurityProtocol - } else { - authMethod = config.NoneAuthMethod - } +func (m *Model) processFormSubmission() tea.Cmd { + m.state = loading + details := m.getRegistrationDetails() - details := config.RegistrationDetails{ - Name: name, - NewName: newName, - Color: m.formValues.Color, - Host: m.formValues.Host, - AuthMethod: authMethod, - SecurityProtocol: securityProtocol, - Username: m.formValues.Username, - Password: m.formValues.Password, - } - if m.formValues.SrEnabled { - details.SchemaRegistry = &config.SchemaRegistryDetails{ - Url: m.formValues.SrUrl, - Username: m.formValues.SrUsername, - Password: m.formValues.SrPassword, - } - } - return m.clusterRegisterer.RegisterCluster(details) + cluster := config.ToCluster(details) + return func() tea.Msg { + return m.connChecker(&cluster) + } +} + +func (m *Model) getRegistrationDetails() config.RegistrationDetails { + var name string + var newName *string + if m.preEditName == nil { // When creating a cluster + name = m.formValues.Name + newName = nil + } else { // When updating a cluster. + name = *m.preEditName + if m.formValues.Name != *m.preEditName { + newName = &m.formValues.Name } } - return cmd + + var authMethod config.AuthMethod + var securityProtocol config.SecurityProtocol + if m.formValues.HasSASLAuthMethodSelected() { + authMethod = config.SASLAuthMethod + securityProtocol = m.formValues.SecurityProtocol + } else { + authMethod = config.NoneAuthMethod + } + + details := config.RegistrationDetails{ + Name: name, + NewName: newName, + Color: m.formValues.Color, + Host: m.formValues.Host, + AuthMethod: authMethod, + SecurityProtocol: securityProtocol, + Username: m.formValues.Username, + Password: m.formValues.Password, + } + if m.formValues.SrEnabled { + details.SchemaRegistry = &config.SchemaRegistryDetails{ + Url: m.formValues.SrUrl, + Username: m.formValues.SrUsername, + Password: m.formValues.SrPassword, + } + } + return details } func (f *FormValues) HasSASLAuthMethodSelected() bool { @@ -279,10 +320,15 @@ func (m *Model) createForm() *huh.Form { return form } -func NewForm(registerer config.ClusterRegisterer, ktx *kontext.ProgramKtx) *Model { +func NewForm( + connChecker kadmin.ConnChecker, + registerer config.ClusterRegisterer, + ktx *kontext.ProgramKtx, +) *Model { var formValues = &FormValues{} model := Model{ - formValues: formValues, + formValues: formValues, + connChecker: connChecker, } model.form = model.createForm() @@ -303,12 +349,32 @@ func NewForm(registerer config.ClusterRegisterer, ktx *kontext.ProgramKtx) *Mode model.authSelectionState = saslSelected } model.srSelectionState = srNothingSelected + + model.notifierCmdBar = cmdbar.NewNotifierCmdBar() + cmdbar.WithMsgHandler(model.notifierCmdBar, func(msg kadmin.ConnectivityCheckStartedMsg, m *notifier.Model) (bool, tea.Cmd) { + return true, m.SpinWithLoadingMsg("Testing cluster connectivity") + }) + cmdbar.WithMsgHandler(model.notifierCmdBar, func(msg kadmin.ConnectionCheckSucceeded, m *notifier.Model) (bool, tea.Cmd) { + return true, m.SpinWithLoadingMsg("Connection success creating cluster") + }) + cmdbar.WithMsgHandler(model.notifierCmdBar, func(msg kadmin.ConnectivityCheckErrMsg, m *notifier.Model) (bool, tea.Cmd) { + model.form = model.createForm() + model.state = none + return true, m.ShowErrorMsg("Cluster not created", msg.Err) + }) + return &model } -func NewEditForm(registerer config.ClusterRegisterer, ktx *kontext.ProgramKtx, formValues *FormValues) *Model { +func NewEditForm( + connChecker kadmin.ConnChecker, + registerer config.ClusterRegisterer, + ktx *kontext.ProgramKtx, + formValues *FormValues, +) *Model { model := Model{ - formValues: formValues, + formValues: formValues, + connChecker: connChecker, } if formValues.Name != "" { // copied to prevent model.preEditedName to follow the formValues.Name pointer @@ -331,5 +397,7 @@ func NewEditForm(registerer config.ClusterRegisterer, ktx *kontext.ProgramKtx, f model.authSelectionState = saslSelected } + model.notifierCmdBar = cmdbar.NewNotifierCmdBar() + return &model } diff --git a/ui/pages/create_cluster_page/upsert_cluster_page_test.go b/ui/pages/create_cluster_page/upsert_cluster_page_test.go index 63591c2..d1a64ba 100644 --- a/ui/pages/create_cluster_page/upsert_cluster_page_test.go +++ b/ui/pages/create_cluster_page/upsert_cluster_page_test.go @@ -11,15 +11,19 @@ import ( "testing" ) -type MockClusterRegisterer struct { +type mockClusterRegisterer struct { } -type CapturedRegistrationDetails struct { +type capturedRegistrationDetails struct { config.RegistrationDetails } -func (m MockClusterRegisterer) RegisterCluster(d config.RegistrationDetails) tea.Msg { - return CapturedRegistrationDetails{d} +func (m mockClusterRegisterer) RegisterCluster(d config.RegistrationDetails) tea.Msg { + return capturedRegistrationDetails{d} +} + +func mockConnChecker(cluster *config.Cluster) tea.Msg { + return cluster } func TestCreateClusterPage(t *testing.T) { @@ -33,7 +37,7 @@ func TestCreateClusterPage(t *testing.T) { t.Run("Display info message when no clusters", func(t *testing.T) { // given - createEnvPage := NewForm(MockClusterRegisterer{}, &ktx) + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &ktx) // then render := createEnvPage.View(&ktx, ui.TestRenderer) @@ -41,7 +45,7 @@ func TestCreateClusterPage(t *testing.T) { }) t.Run("Do not display info message when no clusters", func(t *testing.T) { - createEnvPage := NewForm(MockClusterRegisterer{}, &ktx) + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &ktx) render := createEnvPage.View(&kontext.ProgramKtx{ WindowWidth: 100, @@ -61,7 +65,7 @@ func TestCreateClusterPage(t *testing.T) { t.Run("Name cannot be empty", func(t *testing.T) { // given - createEnvPage := NewForm(MockClusterRegisterer{}, &ktx) + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &ktx) // when createEnvPage.Update(keys.Key(tea.KeyEnter)) @@ -73,7 +77,7 @@ func TestCreateClusterPage(t *testing.T) { t.Run("Name must be unique", func(t *testing.T) { // given - createEnvPage := NewForm(MockClusterRegisterer{}, &kontext.ProgramKtx{ + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &kontext.ProgramKtx{ WindowWidth: 100, WindowHeight: 100, Config: &config.Config{ @@ -108,7 +112,7 @@ func TestCreateClusterPage(t *testing.T) { t.Run("When updating", func(t *testing.T) { t.Run("updates existing cluster fields", func(t *testing.T) { // given - createEnvPage := NewEditForm(MockClusterRegisterer{}, &kontext.ProgramKtx{ + createEnvPage := NewEditForm(mockConnChecker, mockClusterRegisterer{}, &kontext.ProgramKtx{ WindowWidth: 100, WindowHeight: 100, Config: &config.Config{ @@ -158,83 +162,20 @@ func TestCreateClusterPage(t *testing.T) { msg := cmd() // then - assert.IsType(t, CapturedRegistrationDetails{}, msg) + assert.IsType(t, &config.Cluster{}, msg) // and - var updatedName *string - assert.Equal(t, CapturedRegistrationDetails{ - RegistrationDetails: config.RegistrationDetails{ - Name: "prd", - NewName: updatedName, - Color: styles.ColorGreen, - Host: "localhost:9091", - AuthMethod: config.NoneAuthMethod, - }, + assert.Equal(t, &config.Cluster{ + Name: "prd", + Color: styles.ColorGreen, + Active: false, + BootstrapServers: []string{"localhost:9091"}, + SchemaRegistry: nil, }, msg) - - // then - render := createEnvPage.View(&ktx, ui.TestRenderer) - assert.NotContains(t, render, "cluster prd already exists, name most be unique") - }) - - t.Run("updates existing cluster its name", func(t *testing.T) { - // given - createEnvPage := NewEditForm(MockClusterRegisterer{}, &kontext.ProgramKtx{ - WindowWidth: 100, - WindowHeight: 100, - Config: &config.Config{ - Clusters: []config.Cluster{ - { - Name: "prd", - Color: "#808080", - Active: true, - BootstrapServers: []string{":19092"}, - SASLConfig: nil, - }, - { - Name: "tst", - Color: "#F0F0F0", - Active: false, - BootstrapServers: nil, - SASLConfig: nil, - }, - }, - }, - }, &FormValues{ - Name: "prd", - Color: "#808080", - Host: ":9092", - }) - - // when - keys.UpdateKeys(createEnvPage, "2") - cmd := createEnvPage.Update(keys.Key(tea.KeyEnter)) - createEnvPage.Update(cmd()) - // and: select Color - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - createEnvPage.Update(cmd()) - // and: Host is entered - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - createEnvPage.Update(cmd()) - // and: auth method none is selected - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - cmd = createEnvPage.Update(cmd()) - // and: select disabled schema registry - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - cmd = createEnvPage.Update(cmd()) - cmd = createEnvPage.Update(cmd()) - msg := cmd() - - // then - assert.IsType(t, CapturedRegistrationDetails{}, msg) - // and - assert.Equal(t, msg.(CapturedRegistrationDetails).Name, "prd") - assert.Equal(t, *msg.(CapturedRegistrationDetails).NewName, "prd2") - assert.Equal(t, msg.(CapturedRegistrationDetails).Host, ":9092") }) t.Run("name still has to be unique", func(t *testing.T) { // given - createEnvPage := NewEditForm(MockClusterRegisterer{}, &kontext.ProgramKtx{ + createEnvPage := NewEditForm(mockConnChecker, mockClusterRegisterer{}, &kontext.ProgramKtx{ WindowWidth: 100, WindowHeight: 100, Config: &config.Config{ @@ -276,55 +217,9 @@ func TestCreateClusterPage(t *testing.T) { }) }) - t.Run("When no clusters exists", func(t *testing.T) { - - t.Run("Created cluster is active one by default", func(t *testing.T) { - // given - programKtx := &kontext.ProgramKtx{ - WindowWidth: 100, - WindowHeight: 100, - Config: &config.Config{ - Clusters: []config.Cluster{}, - }, - } - createEnvPage := NewForm(MockClusterRegisterer{}, programKtx) - // and: enter name - keys.UpdateKeys(createEnvPage, "PRD") - cmd := createEnvPage.Update(keys.Key(tea.KeyEnter)) - createEnvPage.Update(cmd()) - // and: select Color - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - createEnvPage.Update(cmd()) - // and: Host is entered - keys.UpdateKeys(createEnvPage, "localhost:9092") - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - createEnvPage.Update(cmd()) - // and: auth method none is selected - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - cmd = createEnvPage.Update(cmd()) - cmd = createEnvPage.Update(keys.Key(tea.KeyEnter)) - // and: no schema-registry is selected - cmd = createEnvPage.Update(cmd()) - cmd = createEnvPage.Update(cmd()) - msg := cmd() - - // then - assert.IsType(t, CapturedRegistrationDetails{}, msg) - // and - assert.Equal(t, CapturedRegistrationDetails{ - RegistrationDetails: config.RegistrationDetails{ - Name: "PRD", - Color: styles.ColorGreen, - Host: "localhost:9092", - AuthMethod: config.NoneAuthMethod, - }, - }, msg) - }) - }) - t.Run("Host cannot be empty", func(t *testing.T) { // given - createEnvPage := NewForm(MockClusterRegisterer{}, &kontext.ProgramKtx{ + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &kontext.ProgramKtx{ WindowWidth: 100, WindowHeight: 100, Config: &config.Config{ @@ -355,7 +250,7 @@ func TestCreateClusterPage(t *testing.T) { t.Run("Selecting none auth method creates cluster", func(t *testing.T) { // given - createEnvPage := NewForm(MockClusterRegisterer{}, &kontext.ProgramKtx{ + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &kontext.ProgramKtx{ WindowWidth: 100, WindowHeight: 100, Config: &config.Config{ @@ -392,16 +287,14 @@ func TestCreateClusterPage(t *testing.T) { msg := cmd() // then - assert.IsType(t, CapturedRegistrationDetails{}, msg) + assert.IsType(t, &config.Cluster{}, msg) // and - assert.Equal(t, CapturedRegistrationDetails{ - RegistrationDetails: config.RegistrationDetails{ - Name: "TST", - NewName: nil, - Color: styles.ColorRed, - Host: "localhost:9092", - AuthMethod: config.NoneAuthMethod, - }, + assert.Equal(t, &config.Cluster{ + Name: "TST", + Color: styles.ColorRed, + Active: false, + BootstrapServers: []string{"localhost:9092"}, + SchemaRegistry: nil, }, msg) }) @@ -420,7 +313,7 @@ func TestCreateClusterPage(t *testing.T) { }, }, } - createEnvPage := NewForm(MockClusterRegisterer{}, &programKtx) + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &programKtx) // and: enter name keys.UpdateKeys(createEnvPage, "TST") cmd := createEnvPage.Update(keys.Key(tea.KeyEnter)) @@ -462,7 +355,7 @@ func TestCreateClusterPage(t *testing.T) { }, }, } - createEnvPage := NewForm(MockClusterRegisterer{}, &programKtx) + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &programKtx) // and: enter name keys.UpdateKeys(createEnvPage, "TST") cmd := createEnvPage.Update(keys.Key(tea.KeyEnter)) @@ -508,7 +401,7 @@ func TestCreateClusterPage(t *testing.T) { }, }, } - createEnvPage := NewForm(MockClusterRegisterer{}, &programKtx) + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &programKtx) // and: enter name keys.UpdateKeys(createEnvPage, "TST") cmd := createEnvPage.Update(keys.Key(tea.KeyEnter)) @@ -545,18 +438,18 @@ func TestCreateClusterPage(t *testing.T) { // then assert.Len(t, msgs, 1) - assert.IsType(t, CapturedRegistrationDetails{}, msgs[0]) + assert.IsType(t, &config.Cluster{}, msgs[0]) // and - assert.Equal(t, CapturedRegistrationDetails{ - RegistrationDetails: config.RegistrationDetails{ - Name: "TST", - NewName: nil, - Color: styles.ColorRed, - Host: "localhost:9092", - AuthMethod: config.SASLAuthMethod, - SecurityProtocol: config.SSLSecurityProtocol, + assert.Equal(t, &config.Cluster{ + Name: "TST", + Color: styles.ColorRed, + Active: false, + BootstrapServers: []string{"localhost:9092"}, + SchemaRegistry: nil, + SASLConfig: &config.SASLConfig{ Username: "username", Password: "password", + SecurityProtocol: config.SSLSecurityProtocol, }, }, msgs[0]) }) @@ -576,7 +469,7 @@ func TestCreateClusterPage(t *testing.T) { }, }, } - createEnvPage := NewForm(MockClusterRegisterer{}, &programKtx) + createEnvPage := NewForm(mockConnChecker, mockClusterRegisterer{}, &programKtx) // and: enter name keys.UpdateKeys(createEnvPage, "TST") cmd := createEnvPage.Update(keys.Key(tea.KeyEnter)) @@ -637,24 +530,24 @@ func TestCreateClusterPage(t *testing.T) { keys.UpdateKeys(createEnvPage, "sr-pwd") msgs := keys.Submit(createEnvPage) + // then assert.Len(t, msgs, 1) - assert.IsType(t, CapturedRegistrationDetails{}, msgs[0]) + assert.IsType(t, &config.Cluster{}, msgs[0]) // and - assert.Equal(t, CapturedRegistrationDetails{ - RegistrationDetails: config.RegistrationDetails{ - Name: "TST", - NewName: nil, - Color: styles.ColorRed, - Host: "localhost:9092", - AuthMethod: config.SASLAuthMethod, - SecurityProtocol: config.SSLSecurityProtocol, + assert.Equal(t, &config.Cluster{ + Name: "TST", + Color: styles.ColorRed, + Active: false, + BootstrapServers: []string{"localhost:9092"}, + SASLConfig: &config.SASLConfig{ Username: "username", Password: "password", - SchemaRegistry: &config.SchemaRegistryDetails{ - Url: "sr-url", - Username: "sr-user", - Password: "sr-pwd", - }, + SecurityProtocol: config.SSLSecurityProtocol, + }, + SchemaRegistry: &config.SchemaRegistryConfig{ + Url: "sr-url", + Username: "sr-user", + Password: "sr-pwd", }, }, msgs[0]) }) diff --git a/ui/tabs/clusters_tab/clusters_tab.go b/ui/tabs/clusters_tab/clusters_tab.go index dd96280..8506ae1 100644 --- a/ui/tabs/clusters_tab/clusters_tab.go +++ b/ui/tabs/clusters_tab/clusters_tab.go @@ -5,6 +5,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" "ktea/config" + "ktea/kadmin" "ktea/kontext" "ktea/ui" "ktea/ui/components/statusbar" @@ -16,12 +17,13 @@ import ( type state int type Model struct { - state state - active nav.Page - createPage nav.Page - config *config.Config - statusbar *statusbar.Model - ktx *kontext.ProgramKtx + state state + active nav.Page + createPage nav.Page + config *config.Config + statusbar *statusbar.Model + ktx *kontext.ProgramKtx + connChecker kadmin.ConnChecker } func (m *Model) View(ktx *kontext.ProgramKtx, renderer *ui.Renderer) string { @@ -48,7 +50,9 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd { listPage, _ := clusters_page.New(m.ktx) m.active = listPage m.statusbar = statusbar.New(m.active) - return func() tea.Msg { return config.ReLoadConfig() } + return func() tea.Msg { + return config.ReLoadConfig() + } case config.ClusterDeletedMsg: if m.config.HasClusters() { cmd := m.active.Update(msg) @@ -59,14 +63,14 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd { } }) } else { - m.active = create_cluster_page.NewForm(m.ktx.Config, m.ktx) + m.active = create_cluster_page.NewForm(m.connChecker, m.ktx.Config, m.ktx) } case tea.KeyMsg: switch msg.String() { case "esc": m.active, _ = clusters_page.New(m.ktx) case "ctrl+n": - m.active = create_cluster_page.NewForm(m.ktx.Config, m.ktx) + m.active = create_cluster_page.NewForm(m.connChecker, m.ktx.Config, m.ktx) case "ctrl+e": clusterName := m.active.(*clusters_page.Model).SelectedCluster() selectedCluster := m.ktx.Config.FindClusterByName(*clusterName) @@ -88,6 +92,7 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd { formValues.SrPassword = selectedCluster.SchemaRegistry.Password } m.active = create_cluster_page.NewEditForm( + m.connChecker, m.ktx.Config, m.ktx, formValues, @@ -102,9 +107,13 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd { return m.active.Update(msg) } -func New(ktx *kontext.ProgramKtx) (*Model, tea.Cmd) { +func New( + ktx *kontext.ProgramKtx, + connChecker kadmin.ConnChecker, +) (*Model, tea.Cmd) { var cmd tea.Cmd m := Model{} + m.connChecker = connChecker m.ktx = ktx m.config = ktx.Config if m.config.HasClusters() { @@ -113,7 +122,7 @@ func New(ktx *kontext.ProgramKtx) (*Model, tea.Cmd) { m.active = listPage m.statusbar = statusbar.New(m.active) } else { - m.active = create_cluster_page.NewForm(m.ktx.Config, m.ktx) + m.active = create_cluster_page.NewForm(m.connChecker, m.ktx.Config, m.ktx) } return &m, cmd } diff --git a/ui/tabs/clusters_tab/clusters_tab_test.go b/ui/tabs/clusters_tab/clusters_tab_test.go index ee9bb00..8307bdb 100644 --- a/ui/tabs/clusters_tab/clusters_tab_test.go +++ b/ui/tabs/clusters_tab/clusters_tab_test.go @@ -12,6 +12,10 @@ import ( "testing" ) +func mockConnChecker(cluster *config.Cluster) tea.Msg { + return cluster +} + func TestClustersTab(t *testing.T) { var ktx = kontext.ProgramKtx{ Config: &config.Config{ @@ -26,7 +30,7 @@ func TestClustersTab(t *testing.T) { Config: &config.Config{}, WindowWidth: 0, WindowHeight: 0, - }) + }, mockConnChecker) // when render := clustersTab.View(&ktx, ui.TestRenderer) @@ -53,7 +57,7 @@ func TestClustersTab(t *testing.T) { WindowWidth: 100, WindowHeight: 100, } - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // when render := clustersTab.View(programKtx, ui.TestRenderer) @@ -94,7 +98,7 @@ func TestClustersTab(t *testing.T) { WindowHeight: 100, AvailableHeight: 100, } - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // when render := clustersTab.View(programKtx, ui.TestRenderer) @@ -140,7 +144,7 @@ func TestClustersTab(t *testing.T) { WindowHeight: 100, AvailableHeight: 100, } - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // and table has been initialized clustersTab.View(programKtx, ui.TestRenderer) @@ -193,7 +197,7 @@ func TestClustersTab(t *testing.T) { t.Run("F2 raises delete confirmation", func(t *testing.T) { // given - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // and table has been initialized render := clustersTab.View(programKtx, ui.TestRenderer) @@ -207,7 +211,7 @@ func TestClustersTab(t *testing.T) { t.Run("esc cancels raised delete confirmation", func(t *testing.T) { // given - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // and table has been initialized render := clustersTab.View(programKtx, ui.TestRenderer) // and delete confirmation has been raised @@ -225,7 +229,7 @@ func TestClustersTab(t *testing.T) { t.Run("enter deletes cluster", func(t *testing.T) { // given - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // and table has been initialized render := clustersTab.View(programKtx, ui.TestRenderer) @@ -272,7 +276,7 @@ func TestClustersTab(t *testing.T) { AvailableHeight: 100, } // and - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // and table has been initialized render := clustersTab.View(programKtx, ui.TestRenderer) @@ -319,7 +323,7 @@ func TestClustersTab(t *testing.T) { t.Run("c-e opens edit page", func(t *testing.T) { // given - var clustersTab, _ = New(programKtx) + var clustersTab, _ = New(programKtx, mockConnChecker) // and table has been initialized clustersTab.View(programKtx, ui.TestRenderer)